# ***************************************************************************
# *   Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de>                 *
# *   Copyright (c) 2018 Bernd Hahnebach <bernd@bimstatik.org>              *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   This program is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************

__title__ = "FreeCAD FEM select widget"
__author__ = "Markus Hovorka, Bernd Hahnebach"
__url__ = "http://www.freecadweb.org"

## @package FemSelectWidget
#  \ingroup FEM
#  \brief FreeCAD FEM FemSelectWidget

"This is a temp solution in v0.19dev, to deal with Fem file relocation"

import sys

from PySide import QtGui
from PySide import QtCore

import FreeCAD
import FreeCADGui
import FreeCADGui as Gui

from femtools import geomtools


class _Selector(QtGui.QWidget):

    def __init__(self):
        super(_Selector, self).__init__()
        self._references = []
        self._register = dict()

        addBtn = QtGui.QPushButton(self.tr("Add"))
        delBtn = QtGui.QPushButton(self.tr("Remove"))
        addBtn.clicked.connect(self._add)
        delBtn.clicked.connect(self._del)

        btnLayout = QtGui.QHBoxLayout()
        btnLayout.addWidget(addBtn)
        btnLayout.addWidget(delBtn)

        self._model = QtGui.QStandardItemModel()
        self._view = SmallListView()
        self._view.setModel(self._model)

        self._helpTextLbl = QtGui.QLabel()
        self._helpTextLbl.setWordWrap(True)

        mainLayout = QtGui.QVBoxLayout()
        mainLayout.addWidget(self._helpTextLbl)
        mainLayout.addLayout(btnLayout)
        mainLayout.addWidget(self._view)
        self.setLayout(mainLayout)

    def references(self):
        return [entry for entry in self._references if entry[1]]

    def setReferences(self, references):
        self._references = []
        self._updateReferences(references)

    def setHelpText(self, text):
        self._helpTextLbl.setText(text)

    @QtCore.Slot()
    def _add(self):
        selection = self.getSelection()
        self._updateReferences(selection)

    @QtCore.Slot()
    def _del(self):
        selected = self._view.selectedIndexes()
        for index in selected:
            identifier = self._model.data(index)
            obj, sub = self._register[identifier]
            refIndex = self._getIndex(obj)
            entry = self._references[refIndex]
            newSub = tuple((x for x in entry[1] if x != sub))
            self._references[refIndex] = (obj, newSub)
            self._model.removeRow(index.row())

    def _updateReferences(self, selection):
        for obj, subList in selection:
            index = self._getIndex(obj)
            for sub in subList:
                entry = self._references[index]
                if sub not in entry[1]:
                    self._addToWidget(obj, sub)
                    newEntry = (obj, entry[1] + (sub,))
                    self._references[index] = newEntry

    def _addToWidget(self, obj, sub):
        identifier = "%s::%s" % (obj.Name, sub)
        item = QtGui.QStandardItem(identifier)
        self._model.appendRow(item)
        self._register[identifier] = (obj, sub)

    def _getIndex(self, obj):
        for i, entry in enumerate(self._references):
            if entry[0] == obj:
                return i
        self._references.append((obj, tuple()))
        return len(self._references) - 1

    def getSelection(self):
        raise NotImplementedError()


class BoundarySelector(_Selector):

    def __init__(self):
        super(BoundarySelector, self).__init__()
        self.setWindowTitle(self.tr("Select Faces/Edges/Vertexes"))
        self.setHelpText(self.tr(
            "To add references: select them in the 3D view "
            ' and click "Add".'
        ))

    def getSelection(self):
        selection = []
        for selObj in Gui.Selection.getSelectionEx():
            if selObj.HasSubObjects:
                item = (selObj.Object, tuple(selObj.SubElementNames))
                selection.append(item)
        return selection


class SolidSelector(_Selector):

    def __init__(self):
        super(SolidSelector, self).__init__()
        self.setWindowTitle(self.tr("Select Solids"))
        self.setHelpText(self.tr(
            "Select elements part of the solid that shall be added"
            ' to the list. To add the solid click "Add".'
        ))

    def getSelection(self):
        selection = []
        for selObj in Gui.Selection.getSelectionEx():
            solids = set()
            for sub in self._getObjects(selObj.Object, selObj.SubElementNames):
                s = self._getSolidOfSub(selObj.Object, sub)
                if s is not None:
                    solids.add(s)
            if solids:
                item = (selObj.Object, tuple(solids))
                selection.append(item)
        if len(selection) == 0:
            FreeCAD.Console.PrintMessage(
                "Object with no Shape selected or nothing selected at all.\n"
            )
        return selection

    def _getObjects(self, obj, names):
        objects = []
        if not hasattr(obj, "Shape"):
            FreeCAD.Console.PrintMessage(
                "Selected object has no Shape.\n"
            )
            return objects
        shape = obj.Shape
        for n in names:
            if n.startswith("Face"):
                objects.append(shape.Faces[int(n[4:]) - 1])
            elif n.startswith("Edge"):
                objects.append(shape.Edges[int(n[4:]) - 1])
            elif n.startswith("Vertex"):
                objects.append(shape.Vertexes[int(n[6:]) - 1])
            elif n.startswith("Solid"):
                objects.append(shape.Solids[int(n[5:]) - 1])
        return objects

    def _getSolidOfSub(self, obj, sub):
        foundSolids = set()
        if sub.ShapeType == "Solid":
            for solidId, solid in enumerate(obj.Shape.Solids):
                if sub.isSame(solid):
                    foundSolids.add("Solid" + str(solidId + 1))
        elif sub.ShapeType == "Face":
            for solidId, solid in enumerate(obj.Shape.Solids):
                if self._findSub(sub, solid.Faces):
                    foundSolids.add("Solid" + str(solidId + 1))
        elif sub.ShapeType == "Edge":
            for solidId, solid in enumerate(obj.Shape.Solids):
                if self._findSub(sub, solid.Edges):
                    foundSolids.add("Solid" + str(solidId + 1))
        elif sub.ShapeType == "Vertex":
            for solidId, solid in enumerate(obj.Shape.Solids):
                if self._findSub(sub, solid.Vertexes):
                    foundSolids.add("Solid" + str(solidId + 1))
        if len(foundSolids) == 1:
            it = iter(foundSolids)
            if sys.version_info.major >= 3:
                return next(it)
            else:
                return it.next()
        return None

    def _findSub(self, sub, subList):
        for i, s in enumerate(subList):
            if s.isSame(sub):
                return True
        return False


class SmallListView(QtGui.QListView):

    def sizeHint(self):
        return QtCore.QSize(50, 50)


class GeometryElementsSelection(QtGui.QWidget):

    def __init__(self, ref, eltypes=[], multigeom=True):
        super(GeometryElementsSelection, self).__init__()
        # init ui stuff
        FreeCADGui.Selection.clearSelection()
        self.selection_mode_solid = False
        self.sel_server = None
        self.obj_notvisible = []
        self.initElemTypes(eltypes)
        self.allow_multiple_geom_types = multigeom
        # print(self.allow_multiple_geom_types)
        self.initUI()
        # set references and fill the list widget
        self.references = []
        if ref:
            self.tuplereferences = ref
            self.get_references()
        self.rebuild_list_References()

    def initElemTypes(self, eltypes):
        self.sel_elem_types = eltypes
        # FreeCAD.Console.PrintMessage(
        #     "Selection of: {} is allowed.\n".format(self.sel_elem_types)
        # )
        self.sel_elem_text = ""
        for e in self.sel_elem_types:
            self.sel_elem_text += e + ", "
        self.sel_elem_text = self.sel_elem_text.rstrip(", ")
        # FreeCAD.Console.PrintMessage("Selection of: " + self.sel_elem_text + " is allowed.\n")
        self.selection_mode_std_print_message = (
            "Single click on a " + self.sel_elem_text + " will add it to the list"
        )
        self.selection_mode_solid_print_message = (
            "Single click on a Face or Edge which belongs "
            "to one Solid will add the Solid to the list"
        )

    def initUI(self):
        # auch ArchPanel ist coded ohne ui-file
        # title
        self.setWindowTitle(self.tr("Geometry reference selector for a ") + self.sel_elem_text)
        # button
        self.pushButton_Add = QtGui.QPushButton(self.tr("Add"))
        # label
        self._helpTextLbl = QtGui.QLabel()
        self._helpTextLbl.setWordWrap(True)
        self._helpTextLbl.setText(self.tr(
            'Click on "Add" and select geometric elements to add them to the list. '
            "If no geometry is added to the list, all remaining ones are used. "
            "The following geometry elements are allowed to select: "
        ) + self.sel_elem_text)
        # list
        self.list_References = QtGui.QListWidget()
        # radiobutton down the list
        self.lb_selmod = QtGui.QLabel()
        self.lb_selmod.setText(self.tr("Selection mode"))
        self.rb_standard = QtGui.QRadioButton(self.tr(self.sel_elem_text.lstrip("Solid, ")))
        self.rb_solid = QtGui.QRadioButton(self.tr("Solid"))
        self.rb_standard.setChecked(True)
        self.rb_solid.setChecked(False)
        # radio butoon layout
        rbtnLayout = QtGui.QHBoxLayout()
        rbtnLayout.addWidget(self.lb_selmod)
        rbtnLayout.addWidget(self.rb_standard)
        rbtnLayout.addWidget(self.rb_solid)
        # main layout
        mainLayout = QtGui.QVBoxLayout()
        mainLayout.addWidget(self._helpTextLbl)
        mainLayout.addWidget(self.pushButton_Add)
        mainLayout.addWidget(self.list_References)
        if "Solid" in self.sel_elem_types:
            mainLayout.addLayout(rbtnLayout)
        self.setLayout(mainLayout)
        # signals and slots
        self.list_References.itemSelectionChanged.connect(self.select_clicked_reference_shape)
        self.list_References.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.list_References.connect(
            self.list_References,
            QtCore.SIGNAL("customContextMenuRequested(QPoint)"),
            self.references_list_right_clicked
        )
        QtCore.QObject.connect(
            self.pushButton_Add,
            QtCore.SIGNAL("clicked()"),
            self.add_references
        )
        QtCore.QObject.connect(
            self.rb_standard,
            QtCore.SIGNAL("toggled(bool)"),
            self.choose_selection_mode_standard
        )
        QtCore.QObject.connect(
            self.rb_solid,
            QtCore.SIGNAL("toggled(bool)"),
            self.choose_selection_mode_solid
        )

    def get_references(self):
        for ref in self.tuplereferences:
            for elem in ref[1]:
                self.references.append((ref[0], elem))

    def get_item_text(self, ref):
        return ref[0].Name + ":" + ref[1]

    def get_allitems_text(self):
        items = []
        for ref in self.references:
            items.append(self.get_item_text(ref))
        return sorted(items)

    def rebuild_list_References(self, current_row=0):
        self.list_References.clear()
        for listItemName in self.get_allitems_text():
            self.list_References.addItem(listItemName)
        if current_row > self.list_References.count() - 1:  # first row is 0
            current_row = self.list_References.count() - 1
        if self.list_References.count() > 0:
            self.list_References.setCurrentItem(self.list_References.item(current_row))

    def select_clicked_reference_shape(self):
        self.setback_listobj_visibility()
        if self.sel_server:
            FreeCADGui.Selection.removeObserver(self.sel_server)
            self.sel_server = None
        if not self.sel_server:
            if not self.references:
                return
            currentItemName = str(self.list_References.currentItem().text())
            for ref in self.references:
                if self.get_item_text(ref) == currentItemName:
                    # print("found: shape: " + ref[0].Name + " element: " + ref[1])
                    if not ref[0].ViewObject.Visibility:
                        self.obj_notvisible.append(ref[0])
                        ref[0].ViewObject.Visibility = True
                    FreeCADGui.Selection.clearSelection()
                    ref_sh_type = ref[0].Shape.ShapeType
                    if ref[1].startswith("Solid") and (
                        ref_sh_type == "Compound" or ref_sh_type == "CompSolid"
                    ):
                        # selection of Solids of Compounds or CompSolids is not possible
                        # because a Solid is no Subelement
                        # since only Subelements can be selected
                        # we're going to select all Faces of said Solids
                        # the method getElement(element)doesn't return Solid elements
                        solid = geomtools.get_element(ref[0], ref[1])
                        if not solid:
                            return
                        faces = []
                        for fs in solid.Faces:
                            # find these faces in ref[0]
                            for i, fref in enumerate(ref[0].Shape.Faces):
                                if fs.isSame(fref):
                                    fref_elstring = "Face" + str(i + 1)
                                    if fref_elstring not in faces:
                                        faces.append(fref_elstring)
                        for f in faces:
                            FreeCADGui.Selection.addSelection(ref[0], f)
                    else:
                        # Selection of all other element types is supported
                        FreeCADGui.Selection.addSelection(ref[0], ref[1])

    def setback_listobj_visibility(self):
        """set back Visibility of the list objects
        """
        FreeCADGui.Selection.clearSelection()
        for obj in self.obj_notvisible:
            obj.ViewObject.Visibility = False
        self.obj_notvisible = []

    def references_list_right_clicked(self, QPos):
        self.contextMenu = QtGui.QMenu()
        menu_item_remove_selected = self.contextMenu.addAction("Remove selected geometry")
        menu_item_remove_all = self.contextMenu.addAction("Clear list")
        if not self.references:
            menu_item_remove_selected.setDisabled(True)
            menu_item_remove_all.setDisabled(True)
        self.connect(
            menu_item_remove_selected,
            QtCore.SIGNAL("triggered()"),
            self.remove_selected_reference
        )
        self.connect(
            menu_item_remove_all,
            QtCore.SIGNAL("triggered()"),
            self.remove_all_references
        )
        parentPosition = self.list_References.mapToGlobal(QtCore.QPoint(0, 0))
        self.contextMenu.move(parentPosition + QPos)
        self.contextMenu.show()

    def remove_selected_reference(self):
        if not self.references:
            return
        currentItemName = str(self.list_References.currentItem().text())
        currentRow = self.list_References.currentRow()
        for ref in self.references:
            if self.get_item_text(ref) == currentItemName:
                self.references.remove(ref)
        self.rebuild_list_References(currentRow)

    def remove_all_references(self):
        self.references = []
        self.rebuild_list_References()

    def choose_selection_mode_standard(self, state):
        self.selection_mode_solid = not state
        if self.sel_server and not self.selection_mode_solid:
            FreeCAD.Console.PrintMessage(self.selection_mode_std_print_message + "\n")

    def choose_selection_mode_solid(self, state):
        self.selection_mode_solid = state
        if self.sel_server and self.selection_mode_solid:
            FreeCAD.Console.PrintMessage(self.selection_mode_solid_print_message + "\n")

    def add_references(self):
        """Called if Button add_reference is triggered"""
        # in constraints EditTaskPanel the selection is active as soon as the taskpanel is open
        # here the addReference button EditTaskPanel has to be triggered to start selection mode
        self.setback_listobj_visibility()
        FreeCADGui.Selection.clearSelection()
        # start SelectionObserver and parse the function to add the References to the widget
        if self.selection_mode_solid:  # print message on button click
            print_message = self.selection_mode_solid_print_message
        else:
            print_message = self.selection_mode_std_print_message
        if not self.sel_server:
            # if we do not check, we would start a new SelectionObserver
            # on every click on addReference button
            # but close only one SelectionObserver on leaving the task panel
            self.sel_server = FemSelectionObserver(self.selectionParser, print_message)

    def selectionParser(self, selection):
        FreeCAD.Console.PrintMessage("Selection: {}  {}  {}\n".format(
            selection[0].Shape.ShapeType,
            selection[0].Name,
            selection[1]
        ))
        if hasattr(selection[0], "Shape") and selection[1]:
            sobj = selection[0]
            elt = sobj.Shape.getElement(selection[1])
            ele_ShapeType = elt.ShapeType
            if self.selection_mode_solid and "Solid" in self.sel_elem_types:
                # in solid selection mode use edges and faces for selection of a solid
                # adapt selection variable to hold the Solid
                solid_to_add = None
                if ele_ShapeType == "Edge":
                    found_eltedge_in_other_solid = False
                    for i, s in enumerate(sobj.Shape.Solids):
                        for e in s.Edges:
                            if elt.isSame(e):
                                if found_eltedge_in_other_solid is False:
                                    solid_to_add = str(i + 1)
                                else:
                                    # could be more than two solids, think of polar pattern
                                    FreeCAD.Console.PrintMessage(
                                        "    Edge belongs to at least two solids: "
                                        " Solid{}, Solid{}\n"
                                        .format(solid_to_add, str(i + 1))
                                    )
                                    solid_to_add = None
                                found_eltedge_in_other_solid = True
                elif ele_ShapeType == "Face":
                    found_eltface_in_other_solid = False
                    for i, s in enumerate(sobj.Shape.Solids):
                        for e in s.Faces:
                            if elt.isSame(e):
                                if not found_eltface_in_other_solid:
                                    solid_to_add = str(i + 1)
                                else:
                                    # AFAIK (bernd) a face can only belong to two solids
                                    FreeCAD.Console.PrintMessage(
                                        "    Face belongs to two solids: Solid{}, Solid{}\n"
                                        .format(solid_to_add, str(i + 1))
                                    )
                                    solid_to_add = None
                                found_eltface_in_other_solid = True
                if solid_to_add:
                    selection = (sobj, "Solid" + solid_to_add)
                    ele_ShapeType = "Solid"
                    FreeCAD.Console.PrintMessage(
                        "    Selection variable adapted to hold the Solid: {}  {}  {}\n"
                        .format(sobj.Shape.ShapeType, sobj.Name, selection[1])
                    )
                else:
                    return
            if ele_ShapeType in self.sel_elem_types:
                if (self.selection_mode_solid and ele_ShapeType == "Solid") \
                        or self.selection_mode_solid is False:
                    if selection not in self.references:
                        # only equal shape types are allowed to add
                        if self.allow_multiple_geom_types is False:
                            if self.has_equal_references_shape_types(ele_ShapeType):
                                self.references.append(selection)
                                self.rebuild_list_References(
                                    self.get_allitems_text().index(self.get_item_text(selection))
                                )
                            else:
                                # selected shape will not added to the list
                                FreeCADGui.Selection.clearSelection()
                        else:  # multiple shape types are allowed to add
                            self.references.append(selection)
                            self.rebuild_list_References(
                                self.get_allitems_text().index(self.get_item_text(selection))
                            )
                    else:
                        # selected shape will not added to the list
                        FreeCADGui.Selection.clearSelection()
                        message = (
                            "    Selection {} is in reference list already!\n"
                            .format(self.get_item_text(selection))
                        )
                        FreeCAD.Console.PrintMessage(message)
                        QtGui.QMessageBox.critical(
                            None,
                            "Geometry already in list",
                            message.lstrip(" ")
                        )
            else:
                # selected shape will not added to the list
                FreeCADGui.Selection.clearSelection()
                message = ele_ShapeType + " is not allowed to add to the list!\n"
                FreeCAD.Console.PrintMessage(message)
                QtGui.QMessageBox.critical(None, "Wrong shape type", message)

    def has_equal_references_shape_types(self, ref_shty=""):
        for ref in self.references:
            # the method getElement(element) does not return Solid elements
            r = geomtools.get_element(ref[0], ref[1])
            if not r:
                FreeCAD.Console.PrintError(
                    "Problem in retrieving element: {} \n".format(ref[1])
                )
                continue
            FreeCAD.Console.PrintLog(
                "  ReferenceShape : {}, {}, {} --> {}\n"
                .format(r.ShapeType, ref[0].Name, ref[0].Label, ref[1])
            )
            if not ref_shty:
                ref_shty = r.ShapeType
            if r.ShapeType != ref_shty:
                message = "Multiple shape types are not allowed in the reference list.\n"
                FreeCAD.Console.PrintMessage(message)
                QtGui.QMessageBox.critical(None, "Multiple ShapeTypes not allowed", message)
                return False
        return True


class FemSelectionObserver:
    """selection observer especially for the needs of geometry reference selection of FEM"""
    def __init__(self, parseSelectionFunction, print_message=""):
        self.parseSelectionFunction = parseSelectionFunction
        FreeCADGui.Selection.addObserver(self)
        FreeCAD.Console.PrintMessage(print_message + "!\n")

    def addSelection(self, docName, objName, sub, pos):
        selected_object = FreeCAD.getDocument(docName).getObject(objName)  # get the obj objName
        self.added_obj = (selected_object, sub)
        # on double click on a vertex of a solid sub is None and obj is the solid
        self.parseSelectionFunction(self.added_obj)
